/*
**
** Copyright (c) 2010 by Lorimark Solutions, LLC.
** All Rights Reserved.
** Send inquiries to: info@lorimarksolutions.com
**
*/

/*
** ADC conversion-complete interrupt-vector
**
** This is the A/D converter processing procedure.  
**  The intent is to set up an interrupt based free running 
**  A/D converter.  When each channel finishes its conversion 
**  this procedure is to read the result into a holding
**  register and setup for the next channel conversion.
**
** One thing to note about the A/D converter channel assignments is
**  that when an A/D channel is changed, the change doesn't go into
**  effect until the current conversion finishes and the next one is
**  started.  This means that the A/D interrupt service routine must
**  keep track of what A/D conversion has just completed and what 
**  conversion is about to start.  This requires two conversion control
**  variables (adcc and adnc... see below).
**
** This code has an optional a/d channel mapping table.  There is 
**  overhead associated with processing this table which may be 
**  undesirable, so therefore the implementation of this feature is 
**  controlled by the define macro ISRADC_HARDWARE_MAP.  The table 
**  maps hardware channels to logical software channels.  The 
**  purpose of this table is multifold: 
**
**  1, it allows the hardware to be laid out in an efficient manner, 
**     where wires are brought to the CPU in a good physical layout.  
**     This usually results in analog channels not necessarily attached 
**     to the physical hardware in a logical manner.  Say for instance
**     an 8-channel digital voltage input device may map physical
**     hardware channels 0..7 in an out-of-order sequence to the MCU
**     for efficient hardware wiring.  The resulting logical channel
**     map might look something like:
**
**       const uint8 admap[ADCHANNELS] PROGMEM =
**         { 5, 4, 3, 2, 1, 0, 7, 6, 8 };
**
**  2, it allows channels to be skipped.  The table contains two parts;
**     a size and a sequence of bytes (channels).  A size of less than
**     8 channels is easy to define, and the sequence of channels is
**     arbitrary.  Therefore, channels can be skipped over entirely
**     and processed in any order.
**
**  3, it allows for special internal a/d registers to be read, 
**     including the internal temperature sensor (channel-8) if 
**     present, and the internal analog reference and gnd signals
**     with no software changes.
**
** There are two static variables in use for the channel counters.  These
**  adcc (a/d current channel) and adnc (a/d next channel).  These variables
**  help keep track of what channel is about to be converted, and what 
**  chanel is currently being converted.  When an A/D completion interrupt
**  occurs, the A/D reading is taken from the adcc value, and the adnc
**  value is calculated for the next channel in the series.
**
** A/D readings are stored in an array table.  The array table structure
**  is defined as follows:
**
**    typedef structure
**    {
**      uint16 raw;
**      uint16 avg;
**      uint16 min;
**      uint16 max;
**    } adata_t;
**
**
** This routine also contains procedures for performing averaging
**  functions, and also determining min and max vaues of the A/D readings.
**  Each one of these functions obviously requires additional overhead, but
**  they process the A/D data on the fly, which may be needed in certain
**  applications.  The Min/Max values are particularily handy, since it
**  makes this routine especially capable of capturing analog glitches.
**
**
** This routine can either run in continuous-conversion mode, or it can
**  be set to turn-off the A/D converter on each pass.  In continuous mode,
**  with a 20Mhz oscillator frequency, the converter ISP will run about
**  every 80uS.  If the converter is to be timer driven, or driven by
**  some other system event, then it can be compiled to stop the converter
**  on each pass.  Before the converter is restarted, the channel must
**  be set properly.
**
** Depending on the features selected for the conversion process, this
**  routine will consume 4 to 5 uS processor time, per pass, on a 20Mhz
**  processor.
**
**
** Register allocation is as follows:
**
**    r24,r25      avg/min/max values
**    r26,r27      ADC current value
**    r30,r31      Z register pointer
**
*/

#include <avr/io.h>
#include "isradc.h"

        .global  adcc
        .comm    adcc,1,1

        .global  adnc
        .comm    adnc,1,1

        .extern  adata
        .extern  adcfg

#if defined(ISRADC_HARDWARE_MAP)
.extern admap
#endif

.global ADC_vect
ADC_vect:

        /*
        ** Save our processor state
        **
        */
        push    r1                                    ; unused here, but save it anyway
        push    r0                                    ; use r0 to save the SREG
        in      r0, _SFR_IO_ADDR(SREG)                ; save the SREG
        push    r0                                    ; stack it
        eor     r1, r1                                ; make sure r1 is zero
        push    r24                                   ; avg/min/max L
        push    r25                                   ; avg/min/max H
        push    r26                                   ; A/D value L
        push    r27                                   ; A/D value H
        push    r30                                   ; (ZL)
        push    r31                                   ; (ZH)

        /*
        ** Record the current A/D value in r26, r27
        **
        */
        lds     r26, ADCW                             ; read the A/D low byte first
        lds     r27, ADCW+1                           ; read the A/D high byte


#if defined(ISRADC_STOP_CONVERTING)
        /*
        ** Stop the converter if requested.  The converter
        **  must be restarted manually if this is executed.
        **
        */
        lds     r24, ADCSRA                           ; get the A/D control register
        andi    r24, ~(1<<ADSC)                       ; mask off the Start Conversion
        sts     ADCSRA, r24                           ; write back to the converter control
#endif

        /*
        ** Read the current channel conversion from 
        **  memory.  This is the channel that caused
        **  the interrupt.
        **
        */
        lds     r24, adcc                             ; get the current channel

#if defined(ISRADC_HARDWARE_MAP)
        /*
        ** Read the A/D converter channel map
        **  from program memory.  This maps the
        **  current channel
        */
        ldi     ZL, lo8(admap)                        ; get pointer to admap in PROGMEM
        ldi     ZH, hi8(admap)                        ;
        add     ZL, r24                               ; add the channel
        adc     ZH, r1                                ; add zero with carry
        lpm     r24, Z                                ; r24 now contains the mapped current channel
#endif


        /*
        ** Take the current channel, and multiply
        **  it by the number of bytes (8) in the table
        **
        */
        add     r24, r24                              ; mult x 2
        add     r24, r24                              ; mult x 4
        add     r24, r24                              ; mult x 8

        /*
        ** Point to the adata table base
        **
        */
        ldi     ZL, lo8(adata)                        ; Pointer Address Low Byte
        ldi     ZH, hi8(adata)                        ; Pointer Address High Byte

        /*
        ** Now add the current channel index offset into
        **  the base table address.  This will give us
        **  a pointer in Z(r30,r31) into the table at the 
        **  correct index position.
        */
        add     r30, r24                              ; add the low bytes together
        adc     r31, r1                               ; add zero to the high byte with carry

        /*
        ** Now store the current A/D raw values there
        **
        */
        std     Z+0, r26                              ; store the low byte
        std     Z+1, r27                              ; store the high byte

#if defined(ISRADC_AVG)
        /*
        ** Store the raw value in the average spot also.
        **  We need to do some averaging here, but for
        **  now we'll just store the raw data.
        **
        */
        std     Z+2, r26                              ; store the low byte
        std     Z+3, r27                              ; store the high byte
#endif


#if defined(ISRADC_MIN)
        /*
        ** Perform the MIN function
        **
        */
        ldd     r24, Z+4                              ; get the current min value low
        ldd     r25, Z+5                              ;  and high bytes
        cp      r26, r24                              ; compare against the current A/D low
        cpc     r27, r25                              ;  and high bytes
        brcc    no_min                                ; if the new reading is not lower...
        std     Z+4, r26                              ; store the new min low byte
        std     Z+5, r27                              ; store the new min high byte
no_min:
#endif

#if defined(ISRADC_MAX)
        /*
        ** Perform the MAX function
        **
        */
        ldd     r24, Z+6                              ; get the current max value low
        ldd     r25, Z+7                              ;  and high bytes
        cp      r24, r26                              ; compare against the current A/D low
        cpc     r25, r27                              ;  and high bytes
        brcc    no_max                                ; if the new reading is not higher...
        std     Z+6, r26                              ; store the new max low byte
        std     Z+7, r27                              ; store the new max high byte
no_max:
#endif


        /*
        ** Remember what the next channel is for the next
        **  conversion.
        **
        */
        lds     r24, adnc                             ; get the next channel
        sts     adcc, r24                             ; store it in the current channel

        /*
        ** Advance to the next channel, and round-robin
        **  back to the first.
        **
        */
        subi    r24, 0xFF                             ; add one to the next channel
        cpi     r24, ISRADC_CHANNELS                  ; test for last channel exceed
        brne    no_mod                                ; branch of not overflowed
        eor     r24, r24                              ; overflow, wrap back to the first
no_mod:                                               ;
        sts     adnc, r24                             ; store the result

        /*
        ** Update the MUX with the next channel for
        **  the next conversion.
        **
        */
        lds     r25, ADMUX                            ; get the current mux
        andi    r25, 0xf0                             ; mask off the mux bits
        or      r25, r24                              ; or-in the new mux bits
        sts     ADMUX, r25                            ; update the mux 
                                                      ;  (mux changes on next interrupt)

        /*
        ** All done
        **
        */
        pop     r31
        pop     r30
        pop     r27
        pop     r26
        pop     r25
        pop     r24
        pop     r0
        out     _SFR_IO_ADDR(SREG), r0
        pop     r0
        pop     r1

        reti


.global ADC_InitMinMax
ADC_InitMinMax:
        /*
        ** Setup to wipe all the min/max values
        **
        */
        ldi     r24, ISRADC_CHANNELS
        ldi     r25, 0xff
        ldi     ZL, lo8(adata)
        ldi     ZH, hi8(adata)

        /*
        ** Wipe the table one element at a time.
        **  Disallow interrupts during this time
        **  so that none of the values get clobbered
        **
        */
        cli
iloop:  std     Z+4, r25
        std     Z+5, r25
        std     Z+6, r1
        std     Z+7, r1

        adiw    r30, 8

        dec     r24
        brne    iloop
        sei

        ret

